Path: blob/master/src/packages/next/pages/vouchers/[id].tsx
1450 views
/*1* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { useEffect, useMemo, useState } from "react";6import Footer from "components/landing/footer";7import Header from "components/landing/header";8import Head from "components/landing/head";9import {10Alert,11Button,12Card,13Divider,14Layout,15Modal,16Space,17Table,18} from "antd";19import withCustomize from "lib/with-customize";20import { Customize } from "lib/customize";21import { Icon } from "@cocalc/frontend/components/icon";22import A from "components/misc/A";23import Loading from "components/share/loading";24import TimeAgo from "timeago-react";25import apiPost from "lib/api/post";26import Avatar from "components/account/avatar";27import type { VoucherCode } from "@cocalc/util/db-schema/vouchers";28import { stringify as csvStringify } from "csv-stringify/sync";29import { currency, human_readable_size } from "@cocalc/util/misc";30import CodeMirror from "components/share/codemirror";31import { trunc } from "lib/share/util";32import useDatabase from "lib/hooks/database";33import Notes from "./notes";34import Help from "components/vouchers/help";35import Copyable from "components/misc/copyable";3637function RedeemURL({ code }) {38const [url, setUrl] = useState<string>("");39useEffect(() => {40if (typeof window !== "undefined") {41setUrl(codeToUrl(code, window.location.href));42}43}, []);4445return (46<Space>47<A href={url}>48<Icon name="external-link" />49</A>{" "}50<Copyable display={`…${code}`} value={url} />51</Space>52);53}5455const COLUMNS = [56{57title: "Redeem URL (share this)",58dataIndex: "url",59key: "redeem",60render: (_, { code }) => <RedeemURL code={code} />,61},62{63title: "Code",64dataIndex: "code",65key: "code",66},67{68title: "Created",69dataIndex: "created",70key: "created",71align: "center",72render: (_, { created }) => (73<>{created == null ? "-" : <TimeAgo datetime={created} />}</>74),75},76{77title: "When Redeemed",78dataIndex: "when_redeemed",79key: "when_redeemed",80align: "center",81render: (_, { when_redeemed }) => (82<>{when_redeemed == null ? "-" : <TimeAgo datetime={when_redeemed} />}</>83),84},85{86title: "Redeemed By",87dataIndex: "redeemed_by",88key: "redeemed_by",89align: "center",90render: (_, { redeemed_by }) => (91<>{redeemed_by ? <Avatar account_id={redeemed_by} /> : undefined}</>92),93},9495{96title: "Canceled",97dataIndex: "canceled",98key: "canceled",99align: "center",100render: (_, { canceled }) => (canceled ? "Yes" : "-"),101},102{103title: "Your Private Notes",104dataIndex: "notes",105key: "notes",106render: (_, { notes, code }) => <Notes notes={notes} code={code} />,107},108] as any;109110type DownloadType = "csv" | "json";111112export default function VoucherCodes({ customize, id }) {113const database = useDatabase({ vouchers: { id, title: null, cost: null } });114const [error, setError] = useState<string>("");115const [loading, setLoading] = useState<boolean>(true);116const [data, setData] = useState<VoucherCode[] | null>(null);117const [showModal, setShowModal] = useState<DownloadType | null>(null);118119useEffect(() => {120setLoading(true);121(async () => {122try {123const { codes } = await apiPost("/vouchers/get-voucher-codes", { id });124setData(codes);125} catch (err) {126setError(`${err}`);127} finally {128setLoading(false);129}130})();131}, []);132133const allCodes = useMemo(() => {134if (!data) return [];135return data.map((x) => x.code);136}, [data]);137138const unusedCodes = useMemo(() => {139if (!data) return [];140return data.filter((x) => !x.when_redeemed).map((x) => x.code);141}, [data]);142143const usedCodes = useMemo(() => {144if (!data) return [];145return data.filter((x) => !!x.when_redeemed).map((x) => x.code);146}, [data]);147148return (149<Customize value={customize}>150<Head title={`Voucher With id=${id}`} />151<DownloadModal152data={data}153id={id}154type={showModal}155onClose={() => setShowModal(null)}156/>157<Layout>158<Header />159<Layout.Content>160<div161style={{162width: "100%",163margin: "10vh 0",164display: "flex",165justifyContent: "center",166}}167>168<Card style={{ background: "#fafafa" }}>169<Space direction="vertical" align="center">170<A href="/vouchers">171<Icon name="gift2" style={{ fontSize: "75px" }} />172</A>173<h1>Voucher: id={id}</h1>174{database.value?.vouchers?.title && (175<h3>Title: {database.value.vouchers.title}</h3>176)}177{database.value?.vouchers != null && (178<div179style={{180margin: "auto",181padding: "15px",182textAlign: "center",183fontSize: "14pt",184}}185>186Each Voucher is Worth{" "}187{currency(database.value?.vouchers?.cost)} in credit.188</div>189)}190<Divider />191192{error && (193<Alert194type="error"195message={error}196showIcon197style={{ width: "100%", marginBottom: "30px" }}198closable199onClose={() => setError("")}200/>201)}202{loading && <Loading />}203{!loading && data && (204<div>205<div206style={{207display: "flex",208justifyContent: "center",209marginBottom: "15px",210}}211>212<Space direction="vertical">213<Space>214<div style={{ width: "200px" }}>215Copy All Codes {`(${allCodes.length})`}216</div>217<Copyable218value={allCodes.join(", ")}219inputWidth={"200px"}220/>221</Space>222<Space>223<div style={{ width: "200px" }}>224Copy Unused Codes {`(${unusedCodes.length})`}225</div>226<Copyable227value={unusedCodes.join(", ")}228inputWidth={"200px"}229/>230</Space>231<Space>232<div style={{ width: "200px" }}>233Copy Redeemed Codes {`(${usedCodes.length})`}234</div>235<Copyable236value={usedCodes.join(", ")}237inputWidth={"200px"}238/>239</Space>240<Space>241<div style={{ width: "200px" }}>242Export all data to CSV243</div>244<Button onClick={() => setShowModal("csv")}>245<Icon name="csv" /> Export to CSV...246</Button>247</Space>248<Space>249<div style={{ width: "200px" }}>250Export all data to JSON251</div>252<Button onClick={() => setShowModal("json")}>253<Icon name="js-square" /> Export to JSON...254</Button>255</Space>256</Space>257</div>258259<Table260columns={COLUMNS}261dataSource={data}262rowKey="code"263pagination={{ defaultPageSize: 50 }}264/>265</div>266)}267{!loading && data?.length == 0 && (268<div>269You have not <A href="/redeem">redeemed any vouchers</A>{" "}270yet.271</div>272)}273<Help />274</Space>275</Card>276</div>277<Footer />278</Layout.Content>279</Layout>280</Customize>281);282}283284export async function getServerSideProps(context) {285const { id } = context.params;286return await withCustomize({ context, props: { id } });287}288289function DownloadModal({ type, data, id, onClose }) {290const [data0, setData0] = useState<VoucherCode[] | null>(data);291useEffect(() => {292if (data == null) return;293if (typeof window == "undefined") return;294setData0(295data.map((x) => {296return { ...x, url: codeToUrl(x.code, window.location.href) };297}),298);299}, [data]);300const path = `vouchers-${id}.${type}`;301const content = useMemo(() => {302if (!type || data0 == null) return "";303if (type == "csv") {304const x = [COLUMNS.map((x) => x.title)].concat(305data0.map((x) => COLUMNS.map((c) => x[c.dataIndex])),306);307return csvStringify(x);308} else if (type == "json") {309return JSON.stringify(data0, undefined, 2);310}311return "";312}, [type, data0]);313314const body = useMemo(() => {315if (!type || !data) {316return null;317}318return (319<div>320<div style={{ margin: "30px", fontSize: "13pt", textAlign: "center" }}>321<a322href={URL.createObjectURL(323new Blob([content], { type: "text/plain" }),324)}325download={path}326>327Download {path} (size: {human_readable_size(content.length)})328</a>329</div>330<CodeMirror331lineNumbers={false}332content={trunc(content, 500)}333filename={path}334/>335</div>336);337}, [type, data, id]);338339return (340<Modal341open={type != null}342onCancel={onClose}343onOk={onClose}344title={<>Export all data to {type ? type.toUpperCase() : ""}</>}345>346{body}347</Modal>348);349}350351function codeToUrl(code, href): string {352let i = href.lastIndexOf("/");353i = href.lastIndexOf("/", i - 1);354return `${href.slice(0, i)}/redeem/${code}`;355}356357358